package nels4561;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.UUID;

import spacewar2.actions.MoveAction;
import spacewar2.actions.MoveToObjectAction;
import spacewar2.actions.SpacewarAction;
import spacewar2.actions.SpacewarPurchaseEnum;
import spacewar2.clients.TeamClient;
import spacewar2.objects.Asteroid;
import spacewar2.objects.Base;
import spacewar2.objects.Beacon;
import spacewar2.objects.Ship;
import spacewar2.objects.SpacewarActionableObject;
import spacewar2.objects.SpacewarObject;
import spacewar2.objects.SpacewarWeapon;
import spacewar2.powerups.SpacewarPowerupEnum;
import spacewar2.shadows.Shadow;
import spacewar2.simulator.Toroidal2DPhysics;
import spacewar2.utilities.Position;

/**
 * Assigns each member of the team to collect an asteroid using A*
 * Beacons and bases are navigated to when necessary 
 * 
 * @author Michael & Thomas
 */
public class AStarAgent extends TeamClient 
{
	/**
	 * Enum list of actions for ships
	 */
	enum Action {MINE, HUNT, BUY_SHIP, BUY_BASE, ACTIVATE_SHIELD, DEACTIVATE_SHIELD, BUY_SHIELD}; 
	/**
	 * Map of target to which the ship is heading
	 */
	HashMap<Ship, SpacewarObject> shipToTargetMap;
	/**
	 * Map of action to which the ship is performing
	 */
	HashMap<Ship, Action> shipToActionMap;
	/**
	 * Map of the movement path to which the ship is performing
	 */
	HashMap<Ship, ArrayList<GridCell>> shipToPathMap;
	/**
	 * The number of timeSteps
	 */
	public int timeSteps;
	/**
	 * The target the ship is aiming for
	 */
	SpacewarObject target;
	/**
	 * The object responsible for coordinating ships
	 */
	ShipCoordination shipCoord;
	
	/**
	 * Shooting fields
	 */
	Random random;
	public static double SHOOT_PROBABILITY = 0.1;
	
	/**
	 * Used temporarily to decide whether or not to shoot enemy ships.
	 * Could still be useful in future.
	 * */
	static final double SHIP_ATTACK_DISTANCE = 250.0;
	static final double MAX_SHIP_ANGLE = 5.0;
	
	/**
	 * ArrayList of ship targets for the ship to choose from.
	 * Each target is in the form of an Egocentric Representation.
	 * Each Egocentric Representation contains a target object and a target value.
	 * */
	ArrayList<EgocentricRepresentation> shipTargets;
	
	/**
	 * Send each ship to a target
	 */
	public Map<UUID, SpacewarAction> getMovementStart(Toroidal2DPhysics space,
			Set<SpacewarActionableObject> actionableObjects) 
			{
		//increment timeSteps
		timeSteps++;
		HashMap<UUID, SpacewarAction> actions = new HashMap<UUID, SpacewarAction>();
		
		shipCoord = new ShipCoordination(space, this.getTeamName());
		
		MoveAction action = null;
		for (SpacewarObject actionable :  actionableObjects)
		{
			if (actionable instanceof Ship) 
			{
				//create cast actionable as our ship
				Ship ship = (Ship) actionable;
				
				//find ship's current path
				ArrayList<GridCell> currentPath = shipToPathMap.get(ship);
			
				//create new grid for timeStep
				Grid myGrid = new Grid(space, ship);

				//do we need to recalculate our a*?
				if (currentPath == null || timeSteps%10==0)
				{	
					//Find the base this ship is assigned to.
					Base shipAssignedBase = null;
					for (SpacewarObject posBase : actionableObjects)
					{
						if (posBase instanceof Base)
						{
							Base base = (Base) posBase;
							ArrayList<Ship> ships = shipCoord._baseToShipAssignments.get(base);
							
							if (ships.contains(ship))
							{
								shipAssignedBase = base;
							}
						}
					}
					
					//Choose our appropriate high level movement action
					Action shipAction = decideShipAction(space, ship, shipAssignedBase);
					
					//We are going to buy a base. So we need to navigate a ship to
					//the new base location
					if (shipAction == Action.BUY_BASE)
					{
						shipToActionMap.put(ship, shipAction);
						target = new Beacon(shipCoord.decideNewBasePosition());
						shipToTargetMap.put(ship, target);
					}
					//We have enough ships to hunt. Set a ship to target and pursue.
					else if( shipAction == Action.HUNT)
					{
						shipToActionMap.put(ship, shipAction);
						target = findHuntTarget(space, ship);
						shipToTargetMap.put(ship, target);
					}
					//We have chosen to mine
					else if (shipAction == Action.MINE)
					{
						//pick our next target
						target = decideNextTarget(space, ship);
						shipToActionMap.put(ship, shipAction);;
						shipToTargetMap.put(ship, target);
					}
					
					//populate our grid using the target
					myGrid.populateGrid(space, target);
					
					//calculate path to target using a*
					GridCell currentCell = myGrid.getCurrentCell();
					ArrayList<GridCell> pathList = CalculateActions(currentCell, target, myGrid, space);
					Position nextLoc = pathList.get(0)._location;
					
					//if we are at a target, use MoveToObjectAction to make cleaner transactions between objects
					//else use regular MoveAction
					if(currentCell.object != null && currentCell.object.getId() == target.getId())
					{
						action = new MoveToObjectAction(space, ship.getPosition(), target);
					}
					else {
						action = new MoveAction(space, currentCell._location, nextLoc, 
								space.findShortestDistanceVector(currentCell._location, nextLoc).multiply(2.0));
					}
					
					//map ship to new currentPath
					shipToPathMap.put(ship, pathList);
				}
				else
				{
					GridCell nextPathCell = currentPath.get(0);
					
					//remove gridCell from path if we are in it
					if(myGrid.getCurrentCell() == nextPathCell)
					{
						currentPath.remove(0);
					}
					
					//if we are at a target, use MoveToObjectAction to make cleaner transactions between objects
					//else use regular MoveAction
					if(nextPathCell.object != null && nextPathCell.object.getId() == target.getId())
					{
						action = new MoveToObjectAction(space, ship.getPosition(), target);
					}
					else
					{
						action = new MoveAction(space, ship.getPosition(), nextPathCell._location,
								space.findShortestDistanceVector(ship.getPosition(), nextPathCell._location).multiply(2.0));
					}
					
					//map ship to new currentPath
					shipToPathMap.put(ship, currentPath);
				}
				//put new action in actions map
				actions.put(ship.getId(), action);
			}
		}
		shipToTargetMap.clear();
		return actions;
	}
	/**
	 * Populate all potential targets into the shipTargets ArrayList.
	 * For each potential target in the environment, make an Egocentric Representation out of it.
	 * 
	 * @param space: spacewar environment
	 * @param ship: current ship
	 * */
	private void populateShipTargets(Toroidal2DPhysics space, Ship ship)
	{
		EgocentricRepresentation target;
		
		//calculate all potential Beacon targets
		for(Beacon beacon : space.getBeacons())
		{
			target = new EgocentricRepresentation(beacon, space, shipToTargetMap, ship, timeSteps);
			shipTargets.add(target);
		}
		//calculate all potential Base targets
		for(Base base : space.getBases())
		{
			if(base.getTeamName() == this.getTeamName())
			{
				target = new EgocentricRepresentation(base, space, shipToTargetMap, ship, timeSteps);
				shipTargets.add(target);
			}
		}
		//calculate all potential Asteroid targets
		for(Asteroid asteroid : space.getAsteroids())
		{
			if(asteroid.isMineable())
			{
				target = new EgocentricRepresentation(asteroid, space, shipToTargetMap, ship, timeSteps);
				shipTargets.add(target);
			}
		}
		//calculate all potential Ship targets
		for(Ship loopShip : space.getShips())
		{
			if(loopShip.getTeamName() != ship.getTeamName())
			{
				target = new EgocentricRepresentation(loopShip, space, shipToTargetMap, ship, timeSteps);
				shipTargets.add(target);
			}
		}
	}
	
	/**
	 * Sorts all potential ship targets based on their target values.
	 * */
	private void sortShipTargets()
	{
		//compare Ego Reps in shipTargets using Ego Rep target balues
		Collections.sort(shipTargets, new Comparator<EgocentricRepresentation>(){

			@Override
			public int compare(EgocentricRepresentation arg0,
					EgocentricRepresentation arg1) {
				if(arg0.targetValue < arg1.targetValue)
					return 1;
				return -1;
			}
			
		});
	}
	
	/**
	 * Pick a ship to target. The ship chosen is the closest one
	 * that has money.
	 * @param space
	 * @param ship
	 * @return
	 */
	public Ship findHuntTarget(Toroidal2DPhysics space, Ship ship)
	{
		//hunting variables
		double shortestDist = Double.MAX_VALUE;
		Ship target = null;
		
		//loop through ships to find hunter
		for (Ship s : space.getShips())
		{
			if (!s.getTeamName().equals(this.getTeamName()))
			{
				double dist = space.findShortestDistance(ship.getPosition(), s.getPosition());
				
				if (dist < shortestDist && s.getMoney() > 0)
				{
					shortestDist = dist;
					target = s;
				}
			}
		}
		
		return target;
	}
	
	/**
	 * Use our current state and ship/base assignment to figure out the 
	 * ship's high-level movement action. Uses the powerup planner.
	 * @param space
	 * @param ship
	 * @param base
	 * @return
	 */
	public Action decideShipAction(Toroidal2DPhysics space, Ship ship, Base base)
	{
		PowerupPlanning pp = new PowerupPlanning(space, ship, base);
		
		Set<Ship> keySet = shipToActionMap.keySet();
		
		boolean allowedToBuy = true;
		boolean allowedToHunt = true;
		
		//Checks if any other ship is already performing a hunt or buy action
		//since we only want one ship doing one at a time.
		for (Ship s : keySet)
		{
			if (shipToActionMap.get(s) == Action.BUY_BASE)
			{
				if (!s.equals(ship))
				{
					allowedToBuy = false;
				}
			}
			if (shipToActionMap.get(s) == Action.HUNT)
			{
				if(!s.equals(ship))
				{
					allowedToHunt = false;
				}
			}
		}
		
		if (pp.buyNewBase() && allowedToBuy)
		{
			return Action.BUY_BASE;
		}
		else if (shipCoord._ships.size() == 5 
				&& allowedToHunt && ship.getMoney() == 0
				&& pp.otherLadder)
		{
			return Action.HUNT;
		}
		return Action.MINE;
	}
	
	/**
	 * Chooses next target for agent based on target values.
	 * Target values provided by Global and Egocentric Representations
	 * 
	 * @param space: spacewar environment
	 * @param ship: current ship
	 * */
	public SpacewarObject decideNextTarget(Toroidal2DPhysics space, Ship ship)
	{
		//local variables
		SpacewarObject target;
		SpacewarObject clusterTarget;
		Cluster bestCluster;
		
		//populate the Egocentric Representations
		populateShipTargets(space, ship);
		sortShipTargets();
		
		//create a new Global Representation
		GlobalRepresentation globalRep = new GlobalRepresentation(space, ship);
		bestCluster = globalRep.allClusters.get(0);
		
		//if the cluster contains a beacon, head there first just to be safe
		if(bestCluster.clusterBeacon == null)
		{
			clusterTarget = bestCluster.clusterHub;
		}
		else
		{
			clusterTarget = bestCluster.clusterBeacon;
		}
		
		
		//choose the highest value between the Global and Ego Representations
		if(shipTargets.get(0).targetValue > bestCluster.clusterValue)
			target = shipTargets.get(0).target;
		else
		{
			target = clusterTarget;
		}
		
		//clear ship targets for next timeStep
		shipTargets.clear();
		
		return target;
	}

	/**
	 * Use A* to calculate a path to the current target and search adjacent cells for local targets.
	 * 
	 * @param current: current gridCell of the ship
	 * @param goal: agent's target
	 * @param myGrid: grid for ship
	 * @param space: spacewar environment
	 * @return ArrayList<SpacewarAction>
	 */
	public ArrayList<GridCell> CalculateActions(GridCell current, SpacewarObject goal, Grid myGrid, Toroidal2DPhysics space)
	{
		//function variables
		ArrayList<GridCell> path = new ArrayList<GridCell>();
		//gridcell that aStar is currently in
		GridCell aStarLocation = current;
		int g = 0;
		
		while (true)
		{
			//if the path is too long, break out
			if (g > 150)
			{
				return path;
			}
			
			//find neighbors for aStarLocation
			ArrayList<GridCell> neighborsList = aStarLocation.getNeighborsWithinRadius(myGrid, 1);
			GridCell bestAdjacentCell = null;
			
			//sort the neighbors based on their h values
			Collections.sort(neighborsList, new Comparator<GridCell>() {
			    public int compare(GridCell a, GridCell b) {
			    	if (a.h > b.h)
			    	{
			    		return 1;
			    	}
			    	return -1;
			    }
			});
			
			//if the path finds a different asteroid first, set path for it instead
			for(GridCell neighbor : neighborsList)
			{
				if(neighbor.containsMineableAsteroid)
				{
					bestAdjacentCell = neighbor;
					path.add(bestAdjacentCell);
					return path;
				}
				//if aStar finds target, end algorithm
				if(neighbor.object != null)
				{
					if (neighbor.object.getId() == goal.getId())
					{
						bestAdjacentCell = neighbor;
						path.add(bestAdjacentCell);
						return path;
					}
				}
			}
			
			//if it doesn't find any targets, pick next best cell to go in
			GridCell bestFreeCell = null;
			for (int i = 0; i < neighborsList.size(); i++)
			{
				bestFreeCell = neighborsList.get(i);
				if (!bestFreeCell.avoid)
				{
					break;
				}
			}
			
			//update aStarLocation
			aStarLocation = bestFreeCell;
			//add cell to path to be returned
			path.add(bestFreeCell);
			//increment path cost g
			g++;
		}
	}
	
	/**
	 * Returns the current timeStep that the simulation is on.
	 * 
	 * @return timeSteps
	 * */
	int getTimeSteps()
	{
		return timeSteps;
	}

	@Override
	public void initialize() 
	{
		//Shooting
		random = new Random();
		
		//Initialize Class Variables
		timeSteps = 0;
		shipToPathMap = new HashMap<Ship, ArrayList<GridCell>>();
		shipToActionMap = new HashMap<Ship, Action>();
		shipToTargetMap = new HashMap<Ship, SpacewarObject>();
		shipTargets = new ArrayList<EgocentricRepresentation>();
		target = null;
	}

	@Override
	public void shutDown() {

	}

	@Override
	public void getMovementEnd(Toroidal2DPhysics space,
			Set<SpacewarActionableObject> actionableObjects) 
	{

	}

	@Override
	public Map<UUID, SpacewarPowerupEnum> getPowerups(Toroidal2DPhysics space,
			Set<SpacewarActionableObject> actionableObjects) {
		
		//returned powerup list
		Map<UUID, SpacewarPowerupEnum> powerupMap = new HashMap<UUID, SpacewarPowerupEnum>();
		//planning object used for checking preconditions
		PowerupPlanning pp;
		//base object
		Base thisBase = null;
		//ship object
		Ship thisShip = null;
		
		for(SpacewarObject actionable : actionableObjects)
		{
			//check for base powerups
			if(actionable instanceof Base)
			{
				thisBase = (Base)actionable;
				pp = new PowerupPlanning(space, shipCoord._baseToShipAssignments.get(thisBase).get(0), thisBase);
				
				//should we toggle the shield for our ship?
				if(pp.isEnemyCloseToBase)
					powerupMap.put(thisBase.getId(), SpacewarPowerupEnum.TOGGLE_SHIELD);
				//should we increase the speed of our base regeneration?
				if(pp.currentBase.getEnergy()< PowerupPlanning.BASE_ENERGY_THRESHOLD)
					powerupMap.put(thisBase.getId(), SpacewarPowerupEnum.DOUBLE_BASE_HEALING_SPEED);
					
			}
			//check for ship powerups
			else if(actionable instanceof Ship)
			{
				thisShip = (Ship)actionable;
				for (Base base: shipCoord._bases)
				{
					ArrayList<Ship> baseShips = shipCoord._baseToShipAssignments.get(base);
					if (baseShips.contains(thisShip))
					{
						thisBase = base;
						pp = new PowerupPlanning(space, thisShip, thisBase);
						
						//should we buy a ship shield?
						if(pp.buyShipShield())
							powerupMap.put(thisShip.getId(), SpacewarPowerupEnum.TOGGLE_SHIELD);
					}
				} 
			}
		}
		//powerups for hunting ship
		for (SpacewarObject actionable :  actionableObjects) {
			if (actionable instanceof Ship) {
				Ship myShip = (Ship) actionable;
				
				if (shipToActionMap.get(myShip) == Action.HUNT)
				{
					//are we hunting orcs?
					if (space.findShortestDistance(myShip.getPosition(), 
							shipToTargetMap.get(myShip).getPosition()) < 200
							&& random.nextDouble() < SHOOT_PROBABILITY)
					{
						SpacewarWeapon newBullet = myShip.getNewWeapon(SpacewarPowerupEnum.FIRE_MISSILE);
						if (newBullet != null) {
							powerupMap.put(myShip.getId(), SpacewarPowerupEnum.FIRE_MISSILE);
						}
					}
				}
			}
		}
		return powerupMap;
	}

	/**
	 * Purchases new base when there is enough money.
	 * 
	 * @param space: spacewar environment
	 * @param ships: team ships
	 * @param availableMoney: amount of money the player has
	 * @param currentCostNewBase: obvious
	 * */
	@Override
	public Map<UUID, SpacewarPurchaseEnum> getTeamPurchases(
			Toroidal2DPhysics space,
			Set<SpacewarActionableObject> actionableObjects,
			int availableMoney, Map<SpacewarPurchaseEnum, Integer> purchaseCosts) {
		
		//purchases to be returned
		HashMap<UUID, SpacewarPurchaseEnum> purchases = new HashMap<UUID, SpacewarPurchaseEnum>();
		//team name for planning object
		String teamName;
		//planning object
		PowerupPlanning pp;
		//ship we're evaluating
		Ship thisShip;
		//base tied to the ship
		Base thisBase = null;
		
		for(SpacewarActionableObject actionableObject : actionableObjects)
		{
			if(actionableObject instanceof Ship)
			{
				teamName = ((Ship)actionableObject).getTeamName();
				thisShip = (Ship) actionableObject;
				for (Base base: shipCoord._bases)
				{
					ArrayList<Ship> baseShips = shipCoord._baseToShipAssignments.get(base);
					if (baseShips.contains(thisShip))
					{
						thisBase = base;
						break;
					}
				}
				pp = new PowerupPlanning(space, thisShip, thisBase);
				thisShip = pp.currentShip;
				thisBase = pp.currentBase;
				pp.playerMoney = availableMoney;
				
				//buy a ship?
				//is a new ship already in the purchases list?
				if(pp.buyNewShip() && !purchases.containsValue(SpacewarPurchaseEnum.SHIP))
				{
					purchases.put(thisBase.getId(), SpacewarPurchaseEnum.SHIP);
					availableMoney -= pp.shipCost;
					System.out.println("Purchased ship!");
				}
				//buy a base?
				//is a new base already in the purchases list?
				else if(pp.buyNewBase() && !purchases.containsValue(SpacewarPurchaseEnum.BASE)
						&& shipToActionMap.get(thisShip) == Action.BUY_BASE
						&& space.findShortestDistance(thisShip.getPosition(), shipCoord.decideNewBasePosition()) < 100)
				{
					purchases.put(thisShip.getId(), SpacewarPurchaseEnum.BASE);
					availableMoney -= pp.baseCost;
					shipToActionMap.remove(thisShip);
					System.out.println("Purchased base!");
				}
				//double the base's healing rate?
				else if(pp.buyDoubleBaseHealingRate() && 
						!purchases.containsValue(SpacewarPurchaseEnum.POWERUP_DOUBLE_BASE_HEALING_SPEED))
				{
					System.out.println("Purchased base healing! 1");
					//check for valid purchase
					//if (thisBase.isValidPowerup(SpacewarPurchaseEnum.POWERUP_DOUBLE_BASE_HEALING_SPEED.getPowerupMap()))
					//{
						purchases.put(thisBase.getId(), SpacewarPurchaseEnum.POWERUP_DOUBLE_BASE_HEALING_SPEED);
						availableMoney -= pp.doubleBaseHealingRateCost;
						System.out.println("Purchased base healing! 2");
					//}
				}
				//buy ship shield?
				else if(pp.buyShipShield())
				{
					//check for valid purchase
					if (thisShip.isValidPowerup(SpacewarPurchaseEnum.POWERUP_SHIELD.getPowerupMap()))
					{
						purchases.put(thisShip.getId(), SpacewarPurchaseEnum.POWERUP_SHIELD);
						availableMoney -= pp.shipShieldCost;
					}
				}
				//buy base shield?
				else if(pp.buyBaseShield())
				{
					//check for valid purchase
					if (thisBase.isValidPowerup(SpacewarPurchaseEnum.POWERUP_SHIELD.getPowerupMap()))
					{
						purchases.put(thisBase.getId(), SpacewarPurchaseEnum.POWERUP_SHIELD);
						availableMoney -= pp.baseShieldCost;
					}
				}
			}
		}
		return purchases;
	}
	@Override
	public Set<Shadow> getShadows() {
		// TODO Auto-generated method stub
		return null;
	}

}
